package logger

import (
	"bufio"
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"net/http/httputil"
	"os"
	"os/exec"
	"path/filepath"
	"regexp"
	"runtime"
	"strings"
	"sync"
	"time"

	"github.com/datazip-inc/olake/constants"
	"github.com/rs/zerolog"
	"github.com/spf13/viper"
	"gopkg.in/natefinch/lumberjack.v2"
)

var logger zerolog.Logger

// Info writes record into os.stdout with log level INFO
func Info(v ...interface{}) {
	if len(v) == 1 {
		logger.Info().Interface("message", v[0]).Send()
	} else {
		logger.Info().Msgf("%s", v...)
	}
}

// Info writes record into os.stdout with log level INFO
func Infof(format string, v ...interface{}) {
	logger.Info().Msgf(format, v...)
}

// Debug writes record into os.stdout with log level DEBUG
func Debug(v ...interface{}) {
	logger.Debug().Msgf("%s", v...)
}

// Debugf writes record into os.stdout with log level DEBUG
func Debugf(format string, v ...interface{}) {
	logger.Debug().Msgf(format, v...)
}

// Error writes record into os.stdout with log level ERROR
func Error(v ...interface{}) {
	logger.Error().Msgf("%s", v...)
}

// Fatal writes record into os.stdout with log level ERROR and exits
func Fatal(v ...interface{}) {
	logger.Fatal().Msgf("%s", v...)
	os.Exit(1)
}

// Fatal writes record into os.stdout with log level ERROR
func Fatalf(format string, v ...interface{}) {
	logger.Fatal().Msgf(format, v...)
	os.Exit(1)
}

// Error writes record into os.stdout with log level ERROR
func Errorf(format string, v ...interface{}) {
	logger.Error().Msgf(format, v...)
}

// Warn writes record into os.stdout with log level WARN
func Warn(v ...interface{}) {
	logger.Warn().Msgf("%s", v...)
}

// Warn writes record into os.stdout with log level WARN
func Warnf(format string, v ...interface{}) {
	logger.Warn().Msgf(format, v...)
}

func LogResponse(response *http.Response) {
	respDump, err := httputil.DumpResponse(response, true)
	if err != nil {
		Fatal(err)
	}

	fmt.Println(string(respDump))
}

func LogRequest(req *http.Request) {
	requestDump, err := httputil.DumpRequest(req, true)
	if err != nil {
		Fatal(err)
	}

	fmt.Println(string(requestDump))
}

// CreateFile creates a new file or overwrites an existing one with the specified filename, path, extension,
func FileLogger(content any, fileName, fileExtension string) error {
	configFolder := viper.GetString(constants.ConfigFolder)
	if configFolder == "" {
		return fmt.Errorf("config folder is not set")
	}

	return FileLoggerWithPath(content, filepath.Join(configFolder, fileName+fileExtension))
}

func FileLoggerWithPath(content any, path string) error {
	if path == "" {
		return fmt.Errorf("path is not set")
	}

	// Marshal content to JSON
	contentBytes, err := json.Marshal(content)
	if err != nil {
		return fmt.Errorf("failed to marshal content: %s", err)
	}

	// Create or truncate the file
	file, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
	if err != nil {
		return fmt.Errorf("failed to create or open file: %s", err)
	}
	defer file.Close()

	// Write data to the file
	_, err = file.Write(contentBytes)
	if err != nil {
		return fmt.Errorf("failed to write data to file: %s", err)
	}

	return nil
}

func StatsLogger(ctx context.Context, statsFunc func() (int64, int64, int64)) {
	startTime := time.Now()
	go func() {
		ticker := time.NewTicker(2 * time.Second)
		defer ticker.Stop()
		for {
			select {
			case <-ctx.Done():
				Info("Monitoring stopped")
				return
			case <-ticker.C:
				runningThreads, recordsToSync, syncedRecords := statsFunc()
				memStats := new(runtime.MemStats)
				runtime.ReadMemStats(memStats)
				speed := float64(syncedRecords) / time.Since(startTime).Seconds()
				timeElapsed := time.Since(startTime).Seconds()
				remainingRecords := recordsToSync - syncedRecords
				estimatedSeconds := "Not Determined"
				if speed > 0 && remainingRecords >= 0 {
					estimatedSeconds = fmt.Sprintf("%.2f s", float64(remainingRecords)/speed)
				}
				stats := map[string]interface{}{
					"Writer Threads":           runningThreads,
					"Synced Records":           syncedRecords,
					"Memory":                   fmt.Sprintf("%d mb", memStats.HeapInuse/(1024*1024)),
					"Speed":                    fmt.Sprintf("%.2f rps", speed),
					"Seconds Elapsed":          fmt.Sprintf("%.2f", timeElapsed),
					"Estimated Remaining Time": estimatedSeconds,
				}
				if err := FileLogger(stats, "stats", ".json"); err != nil {
					Fatalf("failed to write stats in file: %s", err)
				}
			}
		}
	}()
}

func Init() {
	// Set up timestamp for log file names
	currentTimestamp := time.Now().UTC()
	timestamp := fmt.Sprintf("%d-%02d-%02d_%02d-%02d-%02d",
		currentTimestamp.Year(), currentTimestamp.Month(), currentTimestamp.Day(),
		currentTimestamp.Hour(), currentTimestamp.Minute(), currentTimestamp.Second())

	// Configure rotating file logs
	rotatingFile := &lumberjack.Logger{
		Filename:   fmt.Sprintf("%s/logs/sync_%s/olake.log", viper.GetString(constants.ConfigFolder), timestamp),
		MaxSize:    100, // Max size in MB
		MaxBackups: 5,   // Number of old log files to retain
		MaxAge:     30,  // Days to retain old log files
		Compress:   true,
	}

	zerolog.TimestampFunc = func() time.Time { return time.Now().UTC() }

	// Log level colors
	logColors := map[string]string{
		"debug": "\033[36m", // Cyan
		"info":  "\033[32m", // Green
		"warn":  "\033[33m", // Yellow
		"error": "\033[31m", // Red
		"fatal": "\033[31m", // Red
	}

	// Thread-safe ConsoleWriter (each goroutine gets its own copy)
	newConsoleWriter := func() zerolog.ConsoleWriter {
		return zerolog.ConsoleWriter{
			Out:        os.Stdout,
			TimeFormat: "2006-01-02 15:04:05",
			FormatLevel: func(i interface{}) string {
				level := strings.ToLower(fmt.Sprintf("%s", i))
				if color, exists := logColors[level]; exists {
					return fmt.Sprintf("%s%s\033[0m", color, strings.ToUpper(level))
				}
				return strings.ToUpper(level)
			},
			FormatMessage: func(i interface{}) string {
				switch v := i.(type) {
				case string:
					return v
				default:
					jsonMsg, err := json.Marshal(v)
					if err != nil {
						return fmt.Sprintf("failed to marshal log message: %s", err)
					}
					return string(jsonMsg)
				}
			},
			FormatTimestamp: func(i interface{}) string {
				return fmt.Sprintf("\033[90m%s\033[0m", i)
			},
		}
	}

	// MultiWriter (rotating file + console writer per goroutine)
	multiWriter := zerolog.MultiLevelWriter(rotatingFile, newConsoleWriter())

	// Create global logger (immutable after init)
	logger = zerolog.New(multiWriter).With().Timestamp().Logger()
}

// ProcessOutputReader is a struct that manages reading output from a process
// and forwarding it to the logger
type ProcessOutputReader struct {
	Name      string // Name to identify the process in logs
	IsError   bool   // Whether this reader handles error output
	reader    *bufio.Scanner
	closeFn   func() error
	closeOnce sync.Once

	// readiness detection only (do not fail on error logs)
	readinessPattern *regexp.Regexp
	readinessCh      chan struct{}
	readinessOnce    sync.Once
	errorLines       sync.Map // map[string]string → procKey -> first error line
}

// NewProcessOutputReader creates a new ProcessOutputReader for a given process
// name is a prefix to identify the process in logs
// isError determines whether to log as Error (true) or Info (false)
// returns the reader and a write end that should be connected to the process output
func NewProcessLogger(name string, isError bool) (*ProcessOutputReader, *os.File, error) {
	r, w, err := os.Pipe()
	if err != nil {
		return nil, nil, fmt.Errorf("failed to create pipe: %v", err)
	}

	reader := &ProcessOutputReader{
		Name:    name,
		IsError: isError,
		reader:  bufio.NewScanner(r),
		closeFn: r.Close,
	}

	return reader, w, nil
}

// StartReading starts reading from the process output in a goroutine
// and logging each line with the appropriate log level
func (p *ProcessOutputReader) StartReading() {
	// Compile regex patterns for Java error detection
	errorLinePattern := regexp.MustCompile(`(?i)(ERROR|FATAL|Exception|Error:|Failed to|java\.lang\.\w+Exception|^\s*Caused by:)`)
	stackTraceLinePattern := regexp.MustCompile(`^\s*at\s+[\w$.]+\([\w$]+\.java:\d+\)`)

	go func() {
		defer p.Close()
		// Track if we're in an error stack trace
		inStackTrace := false

		for p.reader.Scan() {
			line := p.reader.Text()

			// Check if this is an error line
			isErrorLine := p.IsError || errorLinePattern.MatchString(line)
			isStackTraceLine := stackTraceLinePattern.MatchString(line)

			// Determine if we're starting or continuing an error
			if isErrorLine || isStackTraceLine {
				inStackTrace = true
			} else if inStackTrace && !strings.HasPrefix(strings.TrimSpace(line), "at ") {
				// If this is not a stack trace line and doesn't start with "at ",
				// we're likely out of the stack trace
				inStackTrace = false
			}

			// Notify readiness if the configured readiness pattern matches
			if p.readinessPattern != nil && p.readinessCh != nil && p.readinessPattern.MatchString(line) {
				p.readinessOnce.Do(func() {
					select {
					case p.readinessCh <- struct{}{}:
					default:
					}
				})
			}

			if isErrorLine || isStackTraceLine || inStackTrace {
				Error(fmt.Sprintf("%s %s", p.Name, line))
				p.errorLines.LoadOrStore(p.Name, line)
			} else {
				Info(fmt.Sprintf("%s %s", p.Name, line))
			}
		}
	}()
}

// Close closes the reader
func (p *ProcessOutputReader) Close() {
	p.closeOnce.Do(func() {
		if p.closeFn != nil {
			if err := p.closeFn(); err != nil {
				fmt.Printf("Error closing ProcessOutputReader: %v\n", err)
			}
		}
	})
}

// SetupProcessLogger creates stdout and stderr readers for a process
// and returns write-ends that should be connected to the process stdout and stderr
func SetupProcessLogger(processName string) (*ProcessOutputReader, *ProcessOutputReader, *os.File, *os.File, error) {
	// Setup stdout reader
	stdoutReader, stdoutWriter, err := NewProcessLogger(processName, false)
	if err != nil {
		return nil, nil, nil, nil, fmt.Errorf("failed to create stdout reader: %s", err)
	}

	// Setup stderr reader
	stderrReader, stderrWriter, err := NewProcessLogger(processName, true)
	if err != nil {
		stdoutReader.Close()
		if stdoutWriter != nil {
			stdoutWriter.Close()
		}
		return nil, nil, nil, nil, fmt.Errorf("failed to create stderr reader: %s", err)
	}

	return stdoutReader, stderrReader, stdoutWriter, stderrWriter, nil
}

// SetupAndStartProcess creates and starts a process with stdout and stderr logged via the logger.
// It handles the complete process lifecycle including starting the command and managing pipes.
func SetupAndStartProcess(processName string, cmd *exec.Cmd) error {
	// Set up process output capture using the logger utility
	stdoutReader, stderrReader, stdoutWriter, stderrWriter, err := SetupProcessLogger(processName)
	if err != nil {
		return fmt.Errorf("failed to set up process output capture: %s", err)
	}

	// Channels to coordinate readiness or early startup failure
	readyCh := make(chan struct{}, 1)
	errCh := make(chan error, 1)
	readinessPattern := regexp.MustCompile(`(?i)Server started on port`)

	// Configure readers for readiness/error notifications BEFORE starting the process
	stdoutReader.readinessPattern = readinessPattern
	stdoutReader.readinessCh = readyCh

	// do not fail on error lines; only use errCh for early process exit
	stderrReader.readinessPattern = readinessPattern
	stderrReader.readinessCh = readyCh

	// Set the command's stdout and stderr to our pipes
	cmd.Stdout = stdoutWriter
	cmd.Stderr = stderrWriter

	if err := cmd.Start(); err != nil {
		stdoutReader.Close()
		stderrReader.Close()
		stdoutWriter.Close()
		stderrWriter.Close()
		return fmt.Errorf("failed to start process: %s", err)
	}

	// Start reading from the process output
	stdoutReader.StartReading()
	stderrReader.StartReading()

	// Close the write end of the pipes in the parent process
	// since they're only needed by the child process
	stdoutWriter.Close()
	stderrWriter.Close()
	getFirstErrorLine := func(procKey string) string {
		if v, ok := stderrReader.errorLines.Load(procKey); ok {
			return v.(string)
		}
		return ""
	}

	timeoutSec := 30

	ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeoutSec)*time.Second)
	defer cancel()

	done := make(chan struct{})
	// Watch for early process exit before readiness
	go func() {
		// Wait will return when the child process exits (successfully or not)
		wErr := cmd.Wait()
		select {
		case <-done:
			// readiness already confirmed; ignore
			return
		default:
			if wErr != nil {
				select {
				case errCh <- fmt.Errorf("%s exited before ready: %w", processName, wErr):
				default:
				}
			} else {
				select {
				case errCh <- fmt.Errorf("%s exited before ready", processName):
				default:
				}
			}
		}
	}()

	// Block until ready, error, or timeout
	select {
	case <-readyCh:
		close(done)
		// Clear notification channels to avoid further sends
		stdoutReader.readinessCh = nil
		stderrReader.readinessCh = nil
		return nil
	case e := <-errCh:
		close(done)
		// Attempt to stop the process if it is still running
		if cmd.Process != nil {
			_ = cmd.Process.Kill()
		}
		// Include captured error tail from stderr only
		stderrTail := getFirstErrorLine(processName)
		if stderrTail == "" {
			return fmt.Errorf("failed to start iceberg writer: %s", e)
		}
		return fmt.Errorf("failed to start iceberg writer: %s: %s", e, stderrTail)
	case <-ctx.Done():
		close(done)
		if cmd.Process != nil {
			_ = cmd.Process.Kill()
		}
		stderrTail := getFirstErrorLine(processName)
		if stderrTail == "" {
			return fmt.Errorf("iceberg writer %s did not become ready within %d seconds", processName, timeoutSec)
		}
		return fmt.Errorf("iceberg writer %s did not become ready within %d seconds: %s", processName, timeoutSec, stderrTail)
	}
}
